<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        #canvas {
            background: rebeccapurple;
        }
    </style>
</head>
<body>

<canvas id="canvas"></canvas>

<script>
    const canvas = document.getElementById('canvas')
    canvas.width = 800
    canvas.height = 800
    const ctx = canvas.getContext('2d')

    //系列标签
    const seriesLabel = ['T恤', '夹克', '背心', '迷彩服', '防弹衣']
    const seriesData = [112, 1121, 902, 899, 566]
    //饼图圆心位置
    const pos = {x: canvas.width / 2, y: canvas.height / 2}
    const radius = 200;
    //色差
    const colorSpace = Math.floor(150 / seriesLabel.length)

    const labelSum = eval(seriesData.join('+'));

    const seriesAngle = [];
    const seriesColor = [];
    const seriesRadius = [];

    seriesData.forEach((ele, index) => {
        const dataScalar = ele / labelSum;
        const angleLen = dataScalar * Math.PI * 2
        let startAngle = index === 0 ? 0 : seriesAngle[index - 1][1]
        seriesAngle.push([startAngle, startAngle + angleLen])

        //色域
        const gamut = 80 + index * colorSpace;
        const color = `rgba(255,${gamut},${gamut},1)`
        seriesColor.push(color)

        const itemRadius = radius - index * 10;
        seriesRadius.push(itemRadius)
    })

    class Sector {
        constructor(r = 200, startAngle = 0, endAngle = Math.PI / 2, color = 'red') {
            this.r = r
            this.realRadius = r;
            this.expandRad = r + 20;
            this.startAngle = startAngle
            this.endAngle = endAngle
            this.color = color
            this.x = 0
            this.y = 0
            this.text = '标签文字'
            this.textAlign = 'left'
            this.data = 1000

            this.p1 = {x: 0, y: 0, d: 10}
            this.p2 = {x: 0, y: 0, d: 50}
            this.p3 = {x: 0, y: 0, d: 50}
            this.p4 = {x: 0, y: 0, d: 10}

            this.vr = 0;// 半径的方向的运行速度
            this.ar = 0.03;// 半径变化加速度 0.03
            this.bounce = 0.6;// 弹力 0.6

            this.state = 0;// 0-初始状态 1-鼠标划上的状态 2-鼠标离开的状态
        }

        updateRadius(diff) {
            const {state, realRadius, expandRad} = this
            switch (state) {
                case 0:
                    this.shrinkRadius(diff, realRadius)
                    break
                case 1:
                    this.expandRadius(diff, expandRad)
                    break
                case 2:
                    this.shrinkRadius(diff, realRadius)
                    break
            }
        }

        /**
         * 收缩
         */
        shrinkRadius(diff, endR) {
            const {ar, bounce} = this
            this.vr -= ar; // 速度 - 加速度
            this.r += this.vr * diff;
            if (this.r < endR) {
                this.r = endR;
                this.vr *= -bounce; // 让半径向反方向运动
            }
        }

        expandRadius(diff, endR) {
            const {ar, bounce} = this
            this.vr += ar; // 速度+加速度
            this.r += this.vr * diff;
            if (this.r > endR) {
                this.r = endR;
                this.vr *= -bounce; // 让半径向反方向运动
            }
        }

        // 画引导线
        updatePoints() {
            const {x, y, r, startAngle, endAngle, p1, p2, p3, p4} = this
            let dir = startAngle + (endAngle - startAngle) / 2 //扇形中心角度
            if (dir >= Math.PI) {
                dir = dir - 2 * Math.PI
            }

            const l1 = r + p1.d // p1到圆心的距离

            p1.x = Math.cos(dir) * l1 + x //通过三角函数求出 p1 点的 x坐标
            p1.y = Math.sin(dir) * l1 + y //通过三角函数求出 p1 点的 y坐标

            const l2 = r + p2.d
            p2.x = Math.cos(dir) * l2 + x
            p2.y = Math.sin(dir) * l2 + y

            let n = 1;
            if (p1.x > x) {
                this.textAlign = 'left'
            } else {
                n = -1;
                this.textAlign = 'right'
            }

            p3.x = p2.x + p3.d * n
            p3.y = p2.y

            p4.x = p3.x + p4.d * n
            p4.y = p2.y
        }

        draw(ctx) {
            const {x, y, r, startAngle, endAngle, color, p1, p2, p3, p4, text, textAlign} = this
            ctx.save()
            ctx.beginPath()
            ctx.moveTo(x, y)
            ctx.arc(x, y, r, startAngle, endAngle)
            ctx.fillStyle = color
            ctx.fill()

            // 画线
            ctx.beginPath()
            ctx.moveTo(p1.x, p1.y)
            ctx.lineTo(p2.x, p2.y)
            ctx.lineTo(p3.x, p3.y)
            ctx.stroke()

            //文字
            ctx.font = '18px arial'
            ctx.textAlign = textAlign
            ctx.textBaseline = 'middle'
            ctx.fillStyle = '#000'
            ctx.fillText(text, p4.x, p4.y)

            ctx.restore()
        }
    }

    class Tip {
        constructor(fontSize = 32) {
            this.text = '提示内容'
            this.fontSize = fontSize
            this.x = 0
            this.y = 0
            this.padW = 10
            this.padH = 5
            this.visible = false

            this.nx = 0
            this.ny = 0
            this.rate = 0.05
            this.state = 0;
        }

        /**
         * 更新提示位置
         */
        updatePos() {
            const {x, y, nx, ny, rate, state, visible} = this
            if (!visible || !state) return
            this.x += (nx - x) * rate
            this.y += (ny - y) * rate
            const lx = nx - this.x;
            const ly = ny - this.y;
            const len = Math.sqrt(lx * lx + ly * ly)
            if (len < 1) {
                this.state = 0
            }
        }

        draw(ctx) {
            const {text, fontSize, x, y, visible, padW, padH} = this
            if (!visible) return
            ctx.font = `${fontSize}px arial`

            //底色
            const {width} = ctx.measureText(text)
            ctx.fillStyle = 'rgba(0,0,0,0.6)'
            ctx.fillRect(x, y, width + 2 * padW, fontSize + 2 * padH)

            //文字
            ctx.textBaseline = 'top'
            ctx.fillStyle = '#fff'
            ctx.fillText(text, x + padW, y + padH)
        }
    }

    const series = []
    seriesAngle.forEach((ele, index) => {
        const item = new Sector(seriesRadius[index], ele[0], ele[1], seriesColor[index])
        item.x = pos.x
        item.y = pos.y
        item.text = seriesLabel[index]
        item.data = seriesData[index]
        item.updatePoints()
        series.push(item)
    })

    canvas.addEventListener('mousemove', (e) => {
        const mousePos = getPos(e)

        //当前鼠标划上的扇形
        let hoveredItem = null;

        for (let i = 0; i < series.length; i++) {
            // 如果鼠标在扇形内
            if (containPoint(series[i], mousePos)) {
                series[i].state = 1
                hoveredItem = series[i]
            } else if (series[i].state === 1) {
                series[i].state = 2
            }
        }

        if (hoveredItem) {
            tip.visible = true
            tip.state = 1;//鼠标划上状态
            tip.nx = mousePos.x + 10
            tip.ny = mousePos.y + 20
            tip.text = hoveredItem.data
        } else {
            tip.visible = false
        }
    })

    canvas.addEventListener('mousedown', (e) => {
        const mousePos = getPos(e)

        let hoveredItem = null;
        for (let i = 0; i < series.length; i++) {
            if (containPoint(series[i], mousePos)) {
                series[i].state = 1
                hoveredItem = series[i]
            } else if (series[i].state === 1) {
                series[i].state = 2
            }
        }

        if (hoveredItem) {
            tip.visible = true
            tip.state = 1;//鼠标划上状态
            tip.nx = mousePos.x + 10
            tip.ny = mousePos.y + 20
            tip.text = hoveredItem.data
        } else {
            tip.visible = false
        }
    })

    const tip = new Tip()
    tip.x = pos.x
    tip.y = pos.y

    let time = new Date();
    render()

    function render() {
        console.log('1111')
        const now = new Date();
        const diff = now - time;
        time = now;

        // 清除画布
        ctx.clearRect(0, 0, canvas.width, canvas.height)

        series.forEach(item => {
            item.updateRadius(diff)
            item.draw(ctx)
        })

        // 更新提示位置
        tip.updatePos()
        // 绘制
        tip.draw(ctx)

        //连续渲染
        requestAnimationFrame(render)
    }

    function containPoint(obj, mousePos) {
        const {x, y, r, startAngle, endAngle} = obj

        const [subX, subY] = [mousePos.x - x, mousePos.y - y]

        //获取鼠标到圆心的距离
        const len = Math.sqrt(subX * subX + subY * subY)
        const b1 = len <= r

        // 鼠标到圆心弧度判断
        let dir = Math.atan2(subY, subX)
        if (dir < 0) {
            dir += 2 * Math.PI
        }
        const b2 = dir >= startAngle && dir <= endAngle
        return b1 && b2
    }

    function getPos(event) {
        const {clientX, clientY} = event
        const {top, left} = canvas.getBoundingClientRect()
        //获取鼠标相对画布的坐标
        const [x, y] = [clientX - left, clientY - top]
        return {x, y}
    }
</script>
</body>
</html>
